use strict;
use warnings;

package My::Tools::BBox;
use Carp qw(confess);

sub new {
    my ( $class, %params ) = @_;
    my %methods;
    my @children;
    my @anchors;
    my ( $name, $width, $height, $offset_x, $offset_y, $is_padding, $canvas ) = (
                            $params{name},     $params{width},   $params{height}, $params{offset_x},
                            $params{offset_y}, $params{padding}, $params{canvas} );
    $methods{_name}       = sub { return $name; };
    $methods{_width}      = sub { return $width; };
    $methods{_height}     = sub { return $height; };
    $methods{_offset_x}   = sub { return ( defined($offset_x) ? $offset_x : 0 ); };
    $methods{_offset_y}   = sub { return ( defined($offset_y) ? $offset_y : 0 ); };
    $methods{_canvas}     = sub { return ($canvas); };
    $methods{_is_padding} = sub { return $is_padding; };
    $methods{_add_child} = sub {
        my ( $self, $child ) = @_;
        if ( $child && $self->can_contain( bbox => $child ) ) {
            push @children, $child;
            return 1;
        }
    };
    $methods{_get_children} = sub {
        my ( $self, %params ) = @_;
        my @result;
        my $include_padding = $params{padding};
        foreach my $child (@children) {
            if ( !$include_padding && $child->is_padding ) {
                next;
            }
            push @result, $child;
        } ## end foreach my $child (@children)
        return @result;
    };
    $methods{_add_anchor} = sub {
        my ( $self, $anchor ) = @_;
        if ( $anchor && $self->can_contain( point => $anchor ) ) {
            push @anchors, $anchor;
            return 1;
        }
    };
    $methods{_get_anchors} = sub {
        my ($self) = @_;
        return @anchors;
    };
    bless( \%methods, $class );
    return \%methods;
} ## end sub new

sub name {
    my ($self) = @_;
    return &{ $self->{_name} }();
}

sub width {
    my ($self) = @_;
    return &{ $self->{_width} }();
}

sub height {
    my ($self) = @_;
    return &{ $self->{_height} }();
}

sub canvas {
    my ($self) = @_;
    my $canvas = &{ $self->{_canvas} }();
    my $stop;
    while ( $canvas && !$stop ) {

        # is the bbox I'm on on another bbox too...?
        if ($canvas) {
            my $parent_canvas = $canvas->canvas;

            # we go up the chain to the topmost root
            if ($parent_canvas) {
                $canvas = $parent_canvas;
            } else {
                $stop = 1;
            }
        }
    }
    return $canvas;
}

sub canvas_x {
    my ($self) = @_;
    my $overlay = $self->canvas || $self;
    return $overlay->max_x;
}

sub canvas_y {
    my ($self) = @_;
    my $overlay = $self->canvas || $self;
    return $overlay->max_y;
}

sub offset_x {
    my ($self) = @_;
    return &{ $self->{_offset_x} }();
}

sub offset_y {
    my ($self) = @_;
    return &{ $self->{_offset_y} }();
}

sub is_padding {
    my ($self) = @_;
    return &{ $self->{_is_padding} }();
}

sub add_child {
    my ( $self, $child ) = @_;
    return &{ $self->{_add_child} }( $self, $child );
}

sub get_children {
    my ( $self, %params ) = @_;
    return &{ $self->{_get_children} }( $self, %params );
}

sub add_anchor {
    my ( $self, $anchor ) = @_;
    return &{ $self->{_add_anchor} }( $self, $anchor );
}

sub get_anchors {
    my ( $self, %params ) = @_;
    return &{ $self->{_get_anchors} }( $self, %params );
}

sub min_x {
    my ($self) = @_;
    return $self->offset_x;
}

sub min_y {
    my ($self) = @_;
    return $self->offset_y;
}

sub max_x {
    my ($self) = @_;
    return $self->offset_x + $self->width;
}

sub max_y {
    my ($self) = @_;
    return $self->offset_y + $self->height;
}

sub can_contain {
    my ( $self, %params ) = @_;
    my $bbox  = $params{bbox};
    my $point = $params{point};
    if ( !defined($bbox) && !defined($point) ) {
        confess "Neither bbox, point passed";
    }

    # does it support the BBox methods?
    if (    defined($bbox)
         && ( $bbox->isa( ref($self) ) || $self->isa( ref($bbox) ) )
         && $bbox->can("width")
         && $bbox->can("height")
         && $bbox->can("min_x")
         && $bbox->can("min_y")
         && $bbox->can("max_x")
         && $bbox->can("max_y") )
    {
        return (    ( $bbox->min_x >= 0 )
                 && ( $bbox->min_y >= 0 )
                 && ( $bbox->max_x <= $self->width )
                 && ( $bbox->max_y <= $self->height ) );
    } elsif ( defined($point) && ref($point) && ref($point) eq 'HASH' ) {

        #it's an anchor
        return (    ( ( defined( $point->{x} ) ? $point->{x} : 0 ) >= 0 )
                 && ( ( defined( $point->{y} ) ? $point->{y} : 0 ) >= 0 )
                 && ( ( defined( $point->{x} ) ? $point->{x} : 0 ) <= $self->width )
                 && ( ( defined( $point->{y} ) ? $point->{y} : 0 ) <= $self->height ) );
    } ## end elsif ( defined($point) && ref($point) && ref...
} ## end sub can_contain

sub coords {
    my ($self) = @_;
    return ( $self->min_x, $self->min_y, $self->max_x, $self->max_y );
}

sub children_minimum_bounding_box {
    my ( $self, %params ) = @_;
    my $include_padding = $params{padding};
    my @children        = $self->get_children( padding => $include_padding );
    my @anchors         = $self->get_anchors();
    my ( $min_x, $min_y, $max_x, $max_y );

    # if there are no children, return
    if ( scalar(@children) ) {

        # we init the max and min vars to the opposite of what they ought to be
        ( $min_x, $min_y, $max_x, $max_y ) = ( $self->max_x, $self->max_y, 0, 0 );
    } else {
        ( $min_x, $min_y, $max_x, $max_y ) = ( 0, 0, 0, 0 );
    }

    # start working on the children
    foreach my $child (@children) {

        # for minimum bounding values we care only about the offset
        $min_x = (   $min_x <= $self->min_x + $child->offset_x
                   ? $min_x
                   : $self->min_x + $child->offset_x
        );
        $min_y = (   $min_y <= $self->min_y + $child->offset_y
                   ? $min_y
                   : $self->min_y + $child->offset_y
        );

        # for maximum bounding values we need to consider size of child too
        $max_x = (   $max_x >= $self->min_x + $child->max_x
                   ? $max_x
                   : $self->min_x + $child->max_x
        );
        $max_y = (   $max_y >= $self->min_y + $child->max_y
                   ? $max_y
                   : $self->min_y + $child->max_y
        );
    } ## end foreach my $child (@children)
    my @padding_sets;

    # we've got the boundaries, now work out the anchor coordinates
    foreach my $anchor (@anchors) {
        my $anchor_x = ( defined( $anchor->{x} ) ? $anchor->{x} : 0 ) + $self->min_x;
        my $anchor_y = ( defined( $anchor->{y} ) ? $anchor->{y} : 0 ) + $self->min_y;
        my $target_x_percent = $anchor->{target_x_percent};
        my $target_y_percent = $anchor->{target_y_percent};
        my $weight           = $anchor->{weight};

        # anchors like everything else are specified within the boundaries of its parent
        my ( $min_x_padding, $max_x_padding ) =
            $self->_derive_padding( $self->min_x + $anchor_x,
                                    $target_x_percent, $min_x, $max_x, 1, 0 )
            if ( defined($target_x_percent) );
        my ( $min_y_padding, $max_y_padding ) =
            $self->_derive_padding( $self->min_y + $anchor_y,
                                    $target_y_percent, $min_y, $max_y, 0, 1 )
            if ( defined($target_y_percent) );
        push @padding_sets,
            {padding          => [ $min_x_padding, $max_x_padding, $min_y_padding, $max_y_padding ],
             weight           => $weight,
             target_x_percent => $target_x_percent,
             target_y_percent => $target_y_percent
            };
    } ## end foreach my $anchor (@anchors)

    # normalise padding
    my ( $min_x_padding, $max_x_padding, $min_y_padding, $max_y_padding ) = (
                                                                 $self->_normalise_padding(
                                                                     padding_sets => \@padding_sets,
                                                                     element      => 0,
                                                                     is_width     => 1
                                                                 ),
                                                                 $self->_normalise_padding(
                                                                     padding_sets => \@padding_sets,
                                                                     element      => 1,
                                                                     is_width     => 1
                                                                 ),
                                                                 $self->_normalise_padding(
                                                                     padding_sets => \@padding_sets,
                                                                     element      => 2,
                                                                     is_height    => 1
                                                                 ),
                                                                 $self->_normalise_padding(
                                                                     padding_sets => \@padding_sets,
                                                                     element      => 3,
                                                                     is_height    => 1
                                                                 ),
    );
    my $mbb = new( ref($self),
                   name     => $self->name,
                   width    => abs( $max_x - $min_x ) + ( $max_x_padding + $min_x_padding ),
                   height   => abs( $max_y - $min_y ) + ( $max_y_padding + $min_y_padding ),
                   offset_x => $min_x - $min_x_padding,
                   offset_y => $min_y - $min_y_padding,
                   canvas   => $self->canvas || $self,
    );

   # add anchors to the new minimum bounding box so that a call to its children_minimum_bounding_box
   # will return the same result - we will simply use the edges as anchors with no weights
    if ( $min_x_padding || $min_y_padding || $max_x_padding || $max_y_padding ) {
        $mbb->add_anchor( { x => $mbb->width / 100, target_x_percent => 1 } );
        $mbb->add_anchor( { y => $mbb->width / 100, target_y_percent => 1 } );
    }

    # refactor children
    foreach my $child (@children) {
        my $new_child = new( ref($child),
                             name     => $child->name,
                             width    => $child->width,
                             height   => $child->height,
                             offset_x => $self->min_x + $child->offset_x - $min_x + $min_x_padding,
                             offset_y => $self->min_y + $child->offset_y - $min_y + $min_y_padding,
                             padding  => $child->is_padding,
        );
        my $rv = $mbb->add_child($new_child);
        if ( !$rv ) {
            confess( "Inconsistency: failed to add refactored child: "
                     . join( ", ", $new_child->coords ) );
        }
    } ## end foreach my $child (@children)
    return $mbb;
} ## end sub children_minimum_bounding_box

sub conform_to_aspect_ratio {
    my ( $self, %params ) = @_;
    my $bbox                      = $params{bbox};
    my $target_width_height_ratio = $params{aspect_ratio};
    my $width_height_ratio        = $bbox->width / $bbox->height;
    my @solutions;

    # determine how much space we've got to work with to each side
    my $max_min_x_offset = $bbox->min_x;
    my $max_max_x_offset = $self->max_x - $bbox->max_x;
    my $max_x_offset = (   $max_min_x_offset >= $max_max_x_offset
                         ? $max_max_x_offset
                         : $max_min_x_offset
    );    # the smaller of the 2
    my $max_min_y_offset = $bbox->min_y;
    my $max_max_y_offset = $self->max_y - $bbox->max_y;
    my $max_y_offset = (   $max_min_y_offset >= $max_max_y_offset
                         ? $max_max_y_offset
                         : $max_min_y_offset
    );    # ditto
    my $current_area = $bbox->width * $bbox->height;

    # evaluate combinations - ie: we retain the width and change the height, and vice versa
    my $width_from_target = $target_width_height_ratio * $bbox->height;

    # determine if we have sufficient space to do it if width is enlarged
    if ( $width_from_target > $bbox->width ) {
        my $x_offset = ( $width_from_target - $bbox->width ) / 2;
        if ( $x_offset <= $max_x_offset ) {

            # we've got a solution
            push @solutions,
                { width    => $width_from_target,
                  height   => $bbox->height,
                  offset_x => $bbox->offset_x - $x_offset,
                  offset_y => $bbox->offset_y,
                };
        } else {

            # it wouldn't fit, so we go back and make height smaller
            # an assertion here is that the new height is obviously smaller
            my $new_height = ( $self->max_x ) / $target_width_height_ratio;
            push @solutions,
                { width    => $self->max_x,
                  height   => $new_height,
                  offset_x => 0,
                  offset_y => $bbox->offset_y + ( $bbox->height - $new_height ),
                };
        } ## end else [ if ( $x_offset <= $max_x_offset )
    } else {

        # width is smaller than before - easy, we crop the sides off then
        my $x_offset = ( $bbox->width - $width_from_target ) / 2;
        push @solutions,
            { width    => $width_from_target,
              height   => $bbox->height,
              offset_x => $bbox->offset_x + $x_offset,
              offset_y => $bbox->offset_y,
            };
    } ## end else [ if ( $width_from_target > $bbox->width )

    # repeat the process for height
    my $height_from_target = $bbox->width / $target_width_height_ratio;
    if ( $height_from_target > $bbox->height ) {
        my $y_offset = ( $width_from_target - $bbox->width ) / 2;
        if ( $y_offset <= $max_y_offset ) {
            push @solutions,
                { width    => $bbox->width,
                  height   => $height_from_target,
                  offset_x => $bbox->offset_x,
                  offset_y => $bbox->offset_y - $y_offset,
                };
        } else {
            my $new_width = $target_width_height_ratio * ( $self->max_y );
            push @solutions,
                { width    => $new_width,
                  height   => $self->max_y,
                  offset_x => $bbox->offset_x + ( $bbox->width - $new_width ),
                  offset_y => 0,
                };
        } ## end else [ if ( $y_offset <= $max_y_offset )
    } else {

        # height is smaller than before - easy, we crop the sides off then
        my $y_offset = ( $bbox->height - $height_from_target ) / 2;
        push @solutions,
            { width    => $bbox->width,
              height   => $height_from_target,
              offset_x => $bbox->offset_x,
              offset_y => $bbox->offset_y + $y_offset,
            };
    } ## end else [ if ( $height_from_target > $bbox->height )

    # the best solution is determined by the smallest change in % area
    my $best_delta_area;
    my $best_solution;

    # iterate through solutions
    foreach my $solution (@solutions) {
        my $area       = $solution->{width} * $solution->{height};
        my $delta_area = ( abs( $area - $current_area ) ) / $current_area;
        if ( !defined($best_solution) ) {
            $best_solution   = $solution;
            $best_delta_area = $delta_area;
        } else {
            if ( $delta_area < $best_delta_area ) {
                $best_solution   = $solution;
                $best_delta_area = $delta_area;
            }
        } ## end else [ if ( !defined($best_solution) )
    } ## end foreach my $solution (@solutions)
    if ( !defined($best_solution) ) {

        # if indeed nothing is found, then return bbox as it is
        return $bbox;
    } else {
        my $delta_offset_x = $best_solution->{offset_x} - $bbox->offset_x;
        my $delta_offset_y = $best_solution->{offset_y} - $bbox->offset_y;
        my $new_bbox = new( ref($bbox),
                            name     => $self->name,
                            width    => $best_solution->{width},
                            height   => $best_solution->{height},
                            offset_x => $best_solution->{offset_x},
                            offset_y => $best_solution->{offset_y},
                            canvas   => $self->canvas || $self,
        );
        my @bbox_children = $bbox->get_children();
        foreach my $child (@bbox_children) {
            my $child_width = (
                              $child->width + $child->offset_x + $delta_offset_x <= $new_bbox->max_x
                              ? $child->width + $child->offset_x + $delta_offset_x
                              : $new_bbox->max_x - $child->offset_x + $delta_offset_x
            );
            my $child_height = (
                             $child->height + $child->offset_y + $delta_offset_y <= $new_bbox->max_y
                             ? $child->height + $child->offset_y + $delta_offset_y
                             : $new_bbox->max_y - $child->offset_y + $delta_offset_y
            );
            if ( $child_width < 0 || $child_height < 0 ) {
                next;
            }
            my $new_child = new( ref($child),
                                 name     => $child->name,
                                 width    => $child_width,
                                 height   => $child_height,
                                 offset_x => $child->offset_x + $delta_offset_x,
                                 offset_y => $child->offset_y + $delta_offset_y,
                                 padding  => $child->is_padding,
            );
            my $rv = $new_bbox->add_child($new_child);
            if ( !$rv ) {

                #confess( "Inconsistency: failed to add refactored child: "
                #          . join( ", ", $new_child->coords ) );
            }
        } ## end foreach my $child (@bbox_children)
        return $new_bbox;
    } ## end else [ if ( !defined($best_solution) )
} ## end sub conform_to_aspect_ratio

sub _children_center {
    my ($self)         = @_;
    my @children       = $self->children;
    my $x_total        = 0;
    my $y_total        = 0;
    my $x_total_weight = 0;
    my $y_total_weight = 0;
    foreach my $child (@children) {
        my $child_x_weight = $child->width;
        my $child_y_weight = $child->height;
        my $child_x_center = ( $child->max_x + $child->min_x ) / 2;
        my $child_y_center = ( $child->max_y + $child->min_y ) / 2;
        $x_total        += $child_x_center * $child_x_weight;
        $x_total_weight += $child_x_weight;
        $y_total        += $child_y_center * $child_y_weight;
        $y_total_weight += $child_y_weight;
    }
    if ( $x_total_weight > 0 && $y_total_weight > 0 ) {
        return ( $x_total / $x_total_weight, $y_total / $y_total_weight );
    }
}

sub _derive_padding {
    my ( $self, @params ) = @_;
    my ( $point, $target_percent, $min, $max, $is_width, $is_height, %params ) = @params;

    # use the boundaries of the underlying bbox if specified
    # this will enable the padding results from this subroutine to exceed
    # the boundaries of 'this' bbox but within the bounds of its attached canvas
    my $base_boundaries = $params{base_boundaries};

    # normalise booleans
    $is_width  = ( $is_width  ? 1 : 0 );
    $is_height = ( $is_height ? 1 : 0 );
    if ( $is_width == $is_height ) {
        confess("Dimension type ambiguious (width/height)");
    }
    my $span = $max - $min;

    # try to figure out which side requires less padding
    my $base_offset;
    my $max_dimension;
    if ($base_boundaries) {
        $base_offset = 0;                                                     # it is simply zero
        $max_dimension = ( $is_width ? $self->canvas_x : $self->canvas_y );
    } else {
        $base_offset   = ( $is_width ? $self->min_x : $self->min_x );
        $max_dimension = ( $is_width ? $self->max_x : $self->max_y );
    }

    # evaluate how much space we've got to each side
    my $max_max_padding      = $max_dimension - $max;
    my $max_min_padding      = $min - $base_offset;
    my $max_from_rp          = ( $point - $min ) / $target_percent * 100 + $min;
    my $min_from_rp          = $max - ( ( $max - $point ) / ( 100 - $target_percent ) * 100 );
    my $proposed_min_padding = $min - $min_from_rp;
    my $proposed_max_padding = $max_from_rp - $max;
    if (    ( $proposed_max_padding <= 0 && $proposed_min_padding <= 0 )
         || ( $proposed_max_padding >= 0 && $proposed_min_padding >= 0 ) )
    {

        # this makes no sense
        confess("Exceptional condition");
    } elsif ( $proposed_max_padding > 0 ) {

        # we can't exceed the bounds of the box
        $proposed_max_padding = (   $proposed_max_padding <= $max_max_padding
                                  ? $proposed_max_padding
                                  : $max_max_padding
        );

        # return a 0 whenever we act on the axis
        return ( 0, $proposed_max_padding );
    } ## end elsif ( $proposed_max_padding > 0 )
    elsif ( $proposed_min_padding > 0 ) {
        $proposed_min_padding = (   $proposed_min_padding <= $max_min_padding
                                  ? $proposed_min_padding
                                  : $max_min_padding
        );

        # return a 0 whenever we act on the axis
        return ( $proposed_min_padding, 0 );
    } ## end elsif ( $proposed_min_padding > 0 )
} ## end sub _derive_padding

sub _normalise_padding {
    my ( $self, %params ) = @_;
    my $padding_sets = $params{padding_sets};
    my $element      = $params{element};

    # normalise booleans
    my $is_width  = ( $params{is_width}  ? 1 : 0 );
    my $is_height = ( $params{is_height} ? 1 : 0 );
    if ( $is_width == $is_height ) {
        confess("Dimension type ambiguious (width/height)");
    }
    my $element_count          = 0;
    my $total_specified_weight = 0;
    my $specified_weight_count = 0;
    my $total_weight           = 0;
    my $average_weight;
    my $total = 0;

    # first derive an average weight to use for those that are not specified
    foreach my $padding_set (@$padding_sets) {
        my $weight  = $padding_set->{weight};
        my $padding = $padding_set->{padding};
        my $target_percent = (   $is_width
                               ? $padding_set->{target_x_percent}
                               : $padding_set->{target_y_percent}
        );
        if ( ( defined( $padding->[$element] ) && !defined($target_percent) )
             || (   !defined( $padding->[$element] )
                  && defined($target_percent) )
            )
        {

            # if the target percentage is not provided, there shouldn't be a defined padding
            # the reverse also holds, because _derive_padding() returns 0 even if it is the padding
            # for a side is unchanged
            confess( "Inconsistent state between padding and target percentage: "
                         . ( defined( $padding->[$element] )
                             ? $padding->[$element]
                             : 'undef'
                         )
                         . '/'
                         . ( defined($target_percent) ? $target_percent : 'undef' ) . '/'
                         . ( $is_width                ? 'width'         : 'height' )
            );
        } ## end if ( ( defined( $padding->[$element] ) && ...
            # if a particular element is not defined, it means that exist is not acted upon
        if ( defined( $padding->[$element] ) ) {

            # there is something to add
            $element_count++;
            if ($weight) {
                $specified_weight_count++;
                $total_specified_weight += $weight;
            }
        } ## end if ( defined( $padding->[$element] ) )
    } ## end foreach my $padding_set (@$padding_sets)
    if ($specified_weight_count) {

        # we won't get divide by zero
        $total_weight =
            $total_specified_weight
            + ( $element_count - $specified_weight_count ) 
            / $specified_weight_count
            * $total_specified_weight;
        $average_weight = $total_weight / $element_count;
    } ## end if ($specified_weight_count)
    else {

        # if none are specified it is safe to use 1, then all will be the same
        $average_weight = 1;
        $total_weight   = $element_count;
    } ## end else [ if ($specified_weight_count)
    foreach my $padding_set (@$padding_sets) {
        my $weight  = $padding_set->{weight};
        my $padding = $padding_set->{padding};
        $padding->[$element] = 0 if ( !defined( $padding->[$element] ) );
        if ( defined( $padding->[$element] ) ) {
            my $weight = ( defined( $padding_set->{weight} )
                           ? $padding_set->{weight}
                           : $average_weight
            );

            # there is something to add
            $total += $padding->[$element] * $weight;
        } ## end if ( defined( $padding->[$element] ) )
    } ## end foreach my $padding_set (@$padding_sets)
    if ($element_count) {
        if ( !$total_weight ) {
            confess "Unhandled exception: total weight zero";
        }
        return ( $total / $total_weight );
    } ## end if ($element_count)
    else {
        return 0;
    }
} ## end sub _normalise_padding

sub _smart_resize {
    my ( $self, %params ) = @_;

    # init params
    my $min             = $params{min};
    my $max             = $params{max};
    my $weighted_center = $params{pivot};
    my $target_span     = $params{target_span};

    # the amount of padding that's available to each side
    my $max_limit = $params{max_limit} - $max;
    my $min_limit = $params{min_limit}
        || $min;    # if not specified we assume everything to the side of min is usable
    my $span             = $max - $min;
    my $pivot_proportion = ( $weighted_center - $min ) / $span;
}
1;
